﻿// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk)
//
// This file is part of SharpMap.
// SharpMap is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// SharpMap is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public License
// along with SharpMap; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 

// SOURCECODE IS MODIFIED FROM ANOTHER WORK AND IS ORIGINALLY BASED ON GeoTools.NET:
/*
 *  Copyright (C) 2002 Urban Science Applications, Inc. 
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * 
 * 
 * updated by asida@centrum.cz for petra.gis
 * 
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Petra.Gis.Geometry;
using Petra.Gis.Geometry.Support;
using Petra.Gis.ReferenceSystemServices;

namespace Petra.Gis.ReferenceSystemServices.Transformations
{
  /// <summary>
  /// <c>ProjectionTransform</c> (cartesian) is base class for transformation between projected coordinate system
  /// and its Spheroid (ellipsoid). Projected coordinate system must have 2 cartesian(orthogonal) x,y axes
  /// thus for polar projection types, conversion equations must be provided inside of transform
  /// to get polar coordinates into x,y cartesian space as well.
  /// Projection transform must be always inversible and inverse transform is defined as geog. to proj.
  /// </summary>
  [DataContract(Name="PROJ")]
  public abstract class ProjectionTransform  : InversibleParametricTransform
  {
    //protected static Hashtable _projections = new Hashtable();
    //public delegate ProjectionTransform getProjection(ProjectedCoordinateSystem pcs);
    //public delegate SRSTransform getProjection(ProjectedCoordinateSystem pcs, bool getAsInverse);


    //local copy of spheroid params
    //protected bool _isInverse = false;
    protected bool _isSpherical = false;
    //protected double _e;
    //protected double _es;
    //protected double _semiMajor;
    //protected double _semiMinor;

    //protected ProjectionTransform _inverse;
    //protected TransformParameters _parameters;

    /// <summary>
    /// this is only for projections that uses distance units
    /// there are also projections (polar,..) that uses angular units or mix of both
    /// polar projection must be always translated into cartesian X,Y inside of transform
    /// </summary>
    private DistanceUnit _projUnit;

    /// <summary>
    /// this is always used by source of projection
    /// </summary>
    private AngularUnit _geogUnit;





    /// <summary>
    /// xaml constr.
    /// </summary>
    protected ProjectionTransform()
      :base() { }





    protected ProjectionTransform(ProjectedCoordinateSystem projCS)
      : this(projCS, false)
    { }





    protected ProjectionTransform(ProjectedCoordinateSystem projCS, bool createInverse)
      : base(projCS, projCS.GeographicCoordinateSystem,
              projCS.ProjectionDef.DistanceParameterUnit,
              projCS.ProjectionDef.AngularParameterUnit,
              createInverse,
              projCS.ProjectionDef.Parameters)
    {
      _projUnit = (DistanceUnit)projCS.Unit;
      _geogUnit = (AngularUnit)projCS.GeographicCoordinateSystem.Unit;
      Projection = projCS;      
    }





    /// <summary>
    /// returns list of axes of ProjectionTransform instance given by equations
    /// </summary>
    public AxisList Axes
    {
      get
      {
        if (projCS != null)
          return projCS.Axes;
        else
          return null;
      }
    }




    /// <summary>
    /// source coordinate system to transform coordinates from
    /// </summary>
    [DataMember(Name="SRC")]
    public override CoordinateSystem Source
    {
      get
      {
        return base.Source;
      }
      set
      {
        validataSourceAndTarget(value, Target);
        base.Source = value;
      }
    }




    /// <summary>
    /// target coordinate system to transform coordinates into
    /// </summary>
    [DataMember(Name = "TARGET")]
    public override CoordinateSystem Target
    {
      get
      {
        return base.Target;
      }
      set
      {
        validataSourceAndTarget(Source, value);
        base.Target = value;
      }
    }





    protected void validataSourceAndTarget(CoordinateSystem src, CoordinateSystem trgt)
    {

      bool isSame = false;

      if (src == null || trgt == null)
        return;

      if (src is ProjectedCoordinateSystem)
      {
        //validateProjectionType(src as ProjectedCoordinateSystem);
        isSame = trgt.EqualParams(((ProjectedCoordinateSystem)src).GeographicCoordinateSystem, CsCompareType.CompareAllParams);
      }
      //else if (trgt is ProjectedCoordinateSystem)
      //{
      //  //validateProjectionType(trgt as ProjectedCoordinateSystem);
      //  isSame = src.EqualParams(((ProjectedCoordinateSystem)trgt).GeographicCoordinateSystem, CsCompareType.CompareAllParams);
      //}
      
      if (!isSame)
        throw new ArgumentException("invalid coordinate system def for ProjectionTransform");
    }





    //protected void validateProjectionType(ProjectedCoordinateSystem pcs)
    //{
    //  if (_className != ((ProjectedCoordinateSystem)pcs).Projection.ClassName)
    //    throw new ArgumentException("source or target ProjectedCoordinateSystem " + pcs.GetType().ToString() +
    //                                " not valid for this " + this.GetType().ToString());
    //}






    protected override bool graphCompleted()
    {
      if (projCS == null)
        return false;

      return !(projCS.GeographicCoordinateSystem == null
        || projCS.ProjectionDef == null);
    }




    //protected override bool parametersValid()
    //{
    //  return projCS.Projection.Parameters == null;
    //}





    //protected override void updateTransform()
    //{

    //  //if (!(projCS.Unit is DistanceUnit))
    //  //  throw new ArgumentException(this.ToString() + " projection must have DistanceUnit");

    //  this._semiMajor = projCS.GeographicCoordinateSystem.Datum.Ellipsoid.SemiMajorAxis;
    //  this._semiMinor = projCS.GeographicCoordinateSystem.Datum.Ellipsoid.SemiMinorAxis;

    //  this._isSpherical = (_semiMajor == _semiMinor);
    //  this._es = 1.0 - (_semiMinor * _semiMinor) / (_semiMajor * _semiMajor);
    //  this._e = Math.Sqrt(_es);

    //}





    private ProjectedCoordinateSystem projCS;


    public ProjectedCoordinateSystem Projection
    {

      // this is a little trick for serialization
      // user can create transform by specifying single projected coordinate system only
      // but this cs is then stored into _source and _target inherited from SRSTransform ;-)
      protected get { return projCS; }

      set
      {
        projCS = value;
        Source = projCS;
        Target = projCS.GeographicCoordinateSystem;

        _projUnit = (DistanceUnit)projCS.Unit;
        _geogUnit = (AngularUnit)projCS.GeographicCoordinateSystem.Unit;

        UpdateTransform();
      }
    }




    //protected int numParameters
    //{
    //  get { return Parameters.Count; }
    //}




    /// <summary>
    /// set/get Math transform parameters
    /// </summary>
    [DataMember(Name = "PARAMS")]
    public override TransformParameters Parameters
    {
      get 
      {
        if (projCS == null)
          return null;

        if (projCS.ProjectionDef == null)
          return null;

        return projCS.ProjectionDef.Parameters; 
      }
      set
      {
        if (value == null)
          throw new ArgumentNullException("cant set null as parameters");

        if (projCS == null)
          //throw new ArgumentNullException("projected coordinate system is null, cant set parameters");
          return;

        if (projCS.ProjectionDef == null)
          //throw new ArgumentNullException("projected coordinate system is incomplete, cant set parameters");
          return;

        projCS.ProjectionDef.Parameters = value;

        //if (value != null)
        UpdateTransform();

      }
    }





    ///// <summary>
    ///// returns true, ProjectionTransform must be always inversible
    ///// </summary>
    //public override bool IsInversible
    //{
    //  get { return true; }
    //}




    //public abstract string[] ProjectionNames { get; }
    //public abstract ProjectionTransform Create(ProjectedCoordinateSystem pcs);

    
    #region ProjectionTransformMembers

    /// <summary>
    /// inherited class must implement this method for
    /// forward transform(projection to geographic).
    /// Method provides transform for normalized coordinate,
    /// where coordinate is already converted from coordinate system specific
    /// units into meters.
    /// </summary>
    /// <param name="p"><c>ICoordinate to transform in meters</c></param>
    protected abstract void projectedToGeographicN(ICoordinate coord);





    /// <summary>
    /// inherited class must implement this method for 
    /// inverse transform (geographic space to projection).
    /// Method provides transform for normalized coordinate,
    /// where coordinate is already converted from coordinate system specific
    /// units into radians.
    /// </summary>
    /// <param name="p"><c>coordinate to transform in radians</c></param>
    protected abstract void geographicToProjectedN(ICoordinate lonlat);




    protected override void transform(ICoordinate coord)
    {
      if (!IsInverse)
      {
        coord.X *= _projUnit.MetersPerUnit;
        coord.Y *= _projUnit.MetersPerUnit;

        projectedToGeographicN(coord);

        coord.lon /= _geogUnit.RadiansPerUnit;
        coord.lat /= _geogUnit.RadiansPerUnit;
      }
      else
      {
        coord.lon *= _geogUnit.RadiansPerUnit;
        coord.lat *= _geogUnit.RadiansPerUnit;

        geographicToProjectedN(coord);

        coord.X /= _projUnit.MetersPerUnit;
        coord.Y /= _projUnit.MetersPerUnit;
      }

    }





    ///// <summary>
    ///// Method provides transformation for <c>CoordinateList</c>.
    ///// Each coordinate is transformed.
    ///// </summary>
    ///// <param name="coordList"></param>
    //public override void Transform(CoordinateList coordList)
    //{

    //  foreach (Coordinate p in coordList)
    //  {
    //    if (IsInverse)
    //      geographicToProjected(p);
    //    else
    //      projectedToGeographic(p);
    //  }
    //}


    


    /// <summary>
    /// some projection transformations might need extra update after create to complete transform equtions.
    /// Oblique transform is sometimes called double transform and requres gauss sphere transform
    /// between projection and spheroid, so whole transform is composed as
    /// projection to gauss sphere, gauss sphere to reference ellipsoid.
    /// </summary>
    public virtual bool IsOblique
    {
      get { return false; }
    }





    /// <summary>
    /// Converts (flatten/develop) SRSTransform into array. 
    /// Some projection might create its own nested transforms
    /// , usually oblique where gauss sphere is applied
    /// between projection and geogr space. This method can remove
    /// all nested transforms and puts them consecutively into simple array
    /// , which has same meaning as nested case.
    /// </summary>
    /// <returns></returns>
    public virtual SRSTransform[] ToArray()
    {
      return new SRSTransform[] { this };
    }


    #endregion ProjectionTransformMembers





    #region projection specific functions


    /// <summary>
    /// Checks whether the values of this instance is equal to the values of another instance.
    /// Only parameters used for coordinate system are used for comparison.
    /// Name, abbreviation, authority, alias and remarks are ignored in the comparison.
    /// </summary>
    /// <param name="obj"></param>
    /// <returns>True if equal</returns>
    public bool EqualParams(object obj)
    {
      if (!(obj is ProjectionTransform))
        return false;

      ProjectionTransform proj = obj as ProjectionTransform;

      if (proj.Parameters.Count != this.Parameters.Count)
        return false;

      foreach (KeyValuePair<string, double> param in this.Parameters)
      {
        if (!proj.Parameters.ContainsKey(param.Key))
          return false;
        else if (param.Value != proj.Parameters[param.Key])
          return false;
      }
      return true;
    }


    #endregion projection specific functions


    //#region Static Methods


    ////public static ProjectionTransform Create(ProjectedCoordinateSystem pcs)
    //public static SRSTransform Create(ProjectedCoordinateSystem pcs, bool getAsInverse)
    //{
    //  getProjection pTransformCreateFn;

    //  // search for registered name of projection transform
    //  try
    //  {
    //    // search with lowercase class name
    //    // through registered projections
    //    pTransformCreateFn = (getProjection)_projections[pcs.Projection.ClassName.ToLower()];
    //  }
    //  catch
    //  {
    //    throw new ArgumentException("Could not create projection transform for : " +
    //                                 pcs.Projection.ClassName);
    //  }

    //  // get projection transform
    //  ProjectionTransform pt = (ProjectionTransform)pTransformCreateFn(pcs, getAsInverse);

    //  // if names and orientation and dimension is same
    //  // just return this transform
    //  if (pt.Axes.EqualParams(pcs.Axes, CsCompareType.CompareAllParams))
    //  {
    //    return pt;
    //  }
    //  else
    //  {
    //    // if axes are totally different throw exception
    //    if (!pt.Axes.EqualParams(pcs.Axes, CsCompareType.SkipProjectionAxesOrientation))
    //      throw new ArgumentException("invalid cs def");

    //    // axes are same but orientation is different
    //    // must prepare XYQuadrantTransform
    //    // clone cs and prepare new axes
    //    ProjectedCoordinateSystem tmpPcs = (ProjectedCoordinateSystem)pcs.Clone();
    //    tmpPcs.Axes = pt.Axes;

    //    // for XYQuadrantTransform is no matter what is source and what is target...
    //    XYQuadrantTransform tmpQt = XYQuadrantTransform.Create(pcs, tmpPcs);
    //    if (tmpQt == null)
    //      throw new ArgumentException("invalid axis def");

    //    TransformList tmpList = new TransformList();
    //    // add projection transform first
    //    tmpList.Add(pt);

    //    if (getAsInverse)
    //      tmpList.Add(tmpQt);
    //    else
    //      tmpList.Insert(0, tmpQt);

    //    ChainedTransform result = new ChainedTransform();
    //    result.CoordinateTransformList = tmpList;

    //    return result;

    //  }
    //}






    //public static bool IsValidProjectionClass(ProjectedCoordinateSystem inputPcs, string[] projectionNames)
    //{
    //  foreach (string sName in projectionNames)
    //    if (inputPcs.Projection.ClassName.ToLower() == sName)
    //      return true;

    //  return false;
    //}





    ///// <summary>
    ///// this function tests wheter transform equation axis definition is same 
    ///// to given projected coordinate system
    ///// if not, then XYQuadrantTransform is utilized...
    ///// </summary>
    ///// <param name="pcs">input projected coordinate system</param>
    ///// <param name="getAsInverse">if transform is requested in reverse order then XYQuadrantTransform must be placed as last</param>
    ///// <param name="projectionDefaultAxes">default projection axes given by projectino equations</param>
    ///// <returns>TransformList with XYQuadrantTransform just TransformList with projection transform only</returns>
    //protected static XYQuadrantTransform getProjectionQuadrantTransform(ProjectedCoordinateSystem inputPcs, AxisList projectionDefaultAxes, bool getAsInverse)
    //{
    //  // now check if projection given in pcs has correct axis definiton
    //  // regarding to equations
    //  // first duplicate input pcs using xaml
    //  string tmp = System.Windows.Markup.XamlWriter.Save(inputPcs);
    //  ProjectedCoordinateSystem tmpCs = (ProjectedCoordinateSystem)System.Windows.Markup.XamlReader.Parse(tmp);
    //  // change its axis def to Krovak axis def
    //  tmpCs.Axes = projectionDefaultAxes;

    //  // if given definition of coordinate system is s-jtsk but with different axis definition
    //  // there must be appended XYQuadrantTransform
    //  if (!inputPcs.EqualParams(tmpCs, CsCompareType.CompareAllParams))
    //  {
    //    XYQuadrantTransform pqt = XYQuadrantTransform.Create(inputPcs, tmpCs);
    //    if (pqt == null)
    //      throw new InvalidOperationException("cant create quadrant transform for " + inputPcs.ToString());
    //    else
    //      return pqt;
    //  }
    //  else
    //    return null;
    //}




    //#endregion Static Methods
  }
}
